#!/usr/bin/python2.4
# Copyright 2011 Google Inc. All Rights Reserved.

""" The datastore schema
"""

__author__ = 'arfuller@google.com (Alfred Fuller)'


import event_hub
import datetime

from google.appengine.ext import db


def transactional(func):
  def runner(*args, **kwds):
    if not db.is_in_transaction():
      return db.run_in_transaction(func, *args, **kwds)
    return func(*args, **kwds)
  return runner


class Friendship(db.Model):

  FRIEND_REQUEST_SENT = 'friend request sent'
  FRIEND_REQUEST_RECEIVED = 'friend request received'
  FRIEND_REQUEST_ACCEPTED = 'friend request accepted'
  FRIEND_REMOVED = 'friend removed'

  PENDING = 0
  ACEPTED = 1

  source_player = db.StringProperty(required=True)
  target_player = db.StringProperty(required=True)
  status = db.IntegerProperty(required=True)

  @classmethod
  def get_key(cls, source_player, target_player):
    ids = [source_player, target_player]
    ids.sort()
    return db.Key.from_path(cls.kind(), '%s|%s' % tuple(ids))

  @classmethod
  def add_friendship(cls, source_player, target_player):
    result = []
    def runner():
      if source_player == target_player:
        return 'Cannot friend yourself'

      key = cls.get_key(source_player, target_player)
      if Friendship.get(key):
        return 'Friendship already exists'

      result.append(cls(key_name=key.name(),
          source_player=source_player,
          target_player=target_player,
          status=cls.PENDING).put())

    error = db.run_in_transaction(runner)
    if error:
      return error

    friendship = result[0]
    event = PlayerEvent(parent=Player.get_key(source_player))
    event.reference = friendship
    event.detail = 'Friend request sent to %s' % Player.get(player_id=target_player).alias
    event.event = cls.FRIEND_REQUEST_SENT
    event_hub.send(event)

    event = PlayerEvent(parent=Player.get_key(target_player))
    event.reference = friendship
    event.detail = 'Friend request received from %s' % Player.get(player_id=source_player).alias
    event.event = cls.FRIEND_REQUEST_RECEIVED
    event_hub.send(event)

  @classmethod
  def are_friends(cls, player1, player2):
    return cls.get(cls.get_key(player1, player2)) is not None

  @classmethod
  def accept_friendship(cls, source_player, target_player=None, reference=None):
    if target_player:
      key = cls.get_key(source_player, target_player)
    else:
      key = db.Key(reference)
    result = []
    def runner():
      friendship = Friendship.get(key)
      if not friendship:
        return 'Unknown friendship'

      if friendship.target_player != source_player:
        return 'Cannot accept your own request'

      friendship.status = cls.ACEPTED
      friendship.put()
      result.append(friendship)

    error = db.run_in_transaction(runner)
    if error:
      return error
    friendship = result[0]

    target_player = friendship.source_player
    event = PlayerEvent(parent=Player.get_key(target_player))
    event.reference = friendship.key()
    event.detail = 'Friend request accepted by %s' % Player.get(player_id=source_player).alias
    event.event = cls.FRIEND_REQUEST_ACCEPTED
    event_hub.send(event)

  @classmethod
  def remove_friendship(cls, source_player, target_player=None, reference=None):
    if target_player:
      key = cls.get_key(source_player, target_player)
    else:
      key = db.Key(reference)
    result = []
    def runner():
      friendship = Friendship.get(key)
      if not friendship:
        return 'Unknown friendship'
      friendship.delete()
      result.append(friendship)

    error = db.run_in_transaction(runner)
    if error:
      return error
    friendship = result[0]

    if source_player == friendship.source_player:
      target_player = friendship.target_player
    else:
      target_player = friendship.source_player


    target_alias = Player.get(player_id=target_player).alias
    source_alias = Player.get(player_id=source_player).alias

    event = PlayerEvent(parent=Player.get_key(source_player))
    event.reference = friendship.key()
    event.detail = 'Remove friendship with %s' % target_alias
    event.event = cls.FRIEND_REMOVED
    event_hub.send(event)

    event = PlayerEvent(parent=Player.get_key(target_player))
    event.reference = friendship.key()
    event.detail = '%s has removed your friendship' % source_alias
    event.event = cls.FRIEND_REMOVED
    event_hub.send(event)

  @classmethod
  def get_friendships(cls, player_id):
    source = Friendship.all().filter('source_player', player_id).filter('status', cls.ACEPTED).fetch(500)
    for friendship in source:
      friendship.friend_id = friendship.target_player
    target = Friendship.all().filter('target_player', player_id).filter('status', cls.ACEPTED).fetch(500)
    for friendship in target:
      friendship.friend_id = friendship.source_player
    return source + target

  @classmethod
  def get_pending_friendships(cls, player_id):
    target = Friendship.all().filter('target_player', player_id).filter('status', cls.PENDING).fetch(500)
    for friendship in target:
      friendship.friend_id = friendship.source_player
    return target


class Player(db.Model):
  ALIAS_CHANGED = 'player alias changed'
  FRIEND_ALIAS_CHANGED = 'friend alias changed'

  MESSAGES = {
      FRIEND_ALIAS_CHANGED: '%s is now known as %s'
  }

  email = db.EmailProperty()
  alias = db.StringProperty()
  skill = db.IntegerProperty()

  def get_id(self):
    return self.key().name()

  @classmethod
  def change_alias(cls, player_id, alias):
    def runner():
      player = cls.get(player_id=player_id)
      if (alias == player.alias):
        return player, None
      prev_alias = player.alias
      player.alias = alias
      player.put()
      return player, prev_alias

    player, prev_alias = db.run_in_transaction(runner)

    if prev_alias:
      event = PlayerEvent(parent=player.key())
      event.event = cls.ALIAS_CHANGED
      event.prev_value = prev_alias
      event.str_value = player.alias
      event.reference = player.key()
      event_hub.send(event, notify_friends)
    return player

  @classmethod
  def get_key(cls, player_id):
    return db.Key.from_path(cls.kind(), player_id)

  @classmethod
  def lookup_id(cls, email):
    key = cls.all(keys_only=True).filter('email', email).get()
    if key:
      return key.name()

  @classmethod
  def get(cls, user=None, email=None, player_id=None):
    if user:
      key_name = 'u%s' % user.user_id()
      existing = cls.get_by_key_name(key_name)
      if existing:
        return existing

      player = Player(key_name=key_name)
      player.skill = 50
      player.email = user.email()
      player.alias = user.email().split('@')[0]
      player.put()
      return player

    if email:
      return cls.all().filter('email', email).get()

    if player_id:
      return cls.get_by_key_name(player_id)

  def __str__(self):
    return '%s (%s)' % (self.alias, self.email)


class PlayerChildEntity(db.Model):
  player_id = db.StringProperty()

  def __init__(self, parent=None, *args, **kwds):
    super(PlayerChildEntity, self).__init__(parent=parent, *args, **kwds)
    if parent:
      assert parent.kind() == Player.kind()
      self.player_id = parent.name()
    else:
      assert self.player_id

  def get_player(self):
    return Player.get(self.key().parent())


class PlayerEvent(PlayerChildEntity):
  event = db.StringProperty()
  game = db.StringProperty()
  detail = db.StringProperty()
  reference = db.ReferenceProperty()
  int_value = db.IntegerProperty()
  str_value = db.StringProperty()

  def __str__(self):
    return '%s [\n%s\n]' % (
      self.__class__, '\n'.join(['%s: %s' % val for val in self._populate_entity().items() if val[1] is not None]))

class PlayerScore(PlayerChildEntity):
  # Primary score events
  NEW_SCORE = 'player score'

  # Secondary events
  NEW_HIGH_SCORE = 'new high score'
  NEW_BEST_TIME = 'new best time'
  NEW_TOP_SCORE_TODAY = 'new today score'
  NEW_TOP_TIME_TODAY = 'new today time'
  NEW_TOP_SCORE = 'new top score'
  NEW_TOP_TIME = 'new top time'
  NEW_NUM_ONE_SCORE = 'new number one score'
  NEW_NUM_ONE_TIME = 'new number one time'

  SCORE_BEATEN = 'friend beat score'
  TIME_BEATEN = 'friend beat time'
  NUM_ONE_SCORE_BEATEN = 'number one score beaten'
  NUM_ONE_TIME_BEATEN = 'number one time beaten'
  TOP_SCORE_BEATEN = 'top score beaten'
  TOP_TIME_BEATEN = 'top time beaten'

  MESSAGES = {
      NEW_HIGH_SCORE: 'You achieved a new high score of %s in %s!',
      NEW_BEST_TIME: 'You achieved a new best time of %s in %s!',
      NEW_TOP_SCORE_TODAY: 'You achieved a new daily top score of %s in %s!',
      NEW_TOP_TIME_TODAY: 'You achieved a new daily top time of %s in %s!',
      NEW_TOP_SCORE: 'You achieved a new top score of %s in %s!',
      NEW_TOP_TIME: 'You achieved a new top time of %s in %s!',
      NEW_NUM_ONE_SCORE: 'You achieved a new #1 score of %s in %s!',
      NEW_NUM_ONE_TIME: 'You achieved a new #1 time of %s in %s!',

      SCORE_BEATEN: '%s just beat your high score of %s with a score of %s in %s!!',
      TIME_BEATEN: '%s just beat your best time of %s with a time of %s in %s!!',
      TOP_SCORE_BEATEN: '%s just beat your top score of %s with a score of %s in %s!!',
      TOP_TIME_BEATEN: '%s just beat your top time of %s with a time of %s in %s!!',
      NUM_ONE_SCORE_BEATEN: '%s just beat your #1 score of %s with a score of %s in %s !!',
      NUM_ONE_TIME_BEATEN: '%s just beat your #1 time of %s with a time of %s in %s!!'}

  game = db.StringProperty()
  score = db.IntegerProperty()
  is_high_score = db.BooleanProperty()
  time = db.IntegerProperty()
  is_best_time = db.BooleanProperty()
  date = db.DateProperty(auto_now_add=True)
  when = db.DateTimeProperty(auto_now_add=True, indexed=False)

  def __str__(self):
    return '%s: %s, %s' % (self.game, self.score, self.time)

  @classmethod
  def record(cls, player_id, game, time, score):
    def runner():
      player_key = Player.get_key(player_id)
      player_score = cls(parent=player_key)
      player_score.game = game
      player_score.time = time
      player_score.score = score

      # checking for high_score
      high_score = cls.get_high_score(player_id, game)
      # checking for best_time
      best_time = cls.get_best_time(player_id, game)

      batch = [player_score]
      if not high_score:
        player_score.is_high_score = True
      elif high_score.score < score:
        player_score.is_high_score = True
        high_score.is_high_score = False
        batch.append(high_score)

      if not best_time:
        player_score.is_best_time = True
      elif best_time.time > time:
        player_score.is_best_time = True
        best_time.is_best_time = False
        batch.append(best_time)

      db.put(batch)
      return (player_score, high_score, best_time)

    player_score, high_score, best_time = db.run_in_transaction(runner)

    event = PlayerEvent(parent=player_score.key().parent())
    event.reference = player_score.key()
    event.game = game
    event.event = cls.NEW_SCORE

    # Sending out score event
    event_hub.send(event, lambda event: cls.check_for_records(player_score, high_score, best_time))

  @classmethod
  def check_for_records(cls, score, prev_high_score, prev_best_time):
    if score.is_high_score:  # New high score!
      event = PlayerEvent(parent=score.key().parent())
      event.game = score.game
      event.reference = score.key()
      event.event = cls.check_for_top_score(score, prev_high_score)
      event.prev_value = prev_high_score
      event.int_value = score.score
      event.detail = cls.MESSAGES[event.event] % (score.score, score.game)
      event_hub.send(event, notify_friends)

    if score.is_best_time:
      event = PlayerEvent(parent=score.key().parent())
      event.game = score.game
      event.reference = score.key()
      event.event = cls.check_for_top_time(score, prev_best_time)
      event.prev_value = prev_best_time
      event.int_value = score.time
      event.detail = cls.MESSAGES[event.event] % (score.time, score.game)
      event_hub.send(event, notify_friends)

  @classmethod
  def check_for_top_score(cls, score, prev_high_score):
    # Grabbing top scores (from other players)
    top_scores = []
    for top_score in cls.get_high_scores(score.game, 6)[0]:
      if ((not prev_high_score or prev_high_score.score < top_score.score) and
          top_score.player_id != score.player_id):
        top_scores.append(top_score)
    top_scores = top_scores[:5]

    if not top_scores:
      return cls.NEW_NUM_ONE_SCORE

    # checking for #1 score
    if score.score > top_scores[0].score:
      cls.notify_number_one_score(score, top_scores)
      return cls.NEW_NUM_ONE_SCORE

    # checking for top score
    if score.score > top_scores[-1].score:
      cls.notify_top_score(score, top_scores)
      return cls.NEW_TOP_SCORE

    # checking for top score today
    top_scores_today = cls.get_high_scores(score.game, 5, True)[0]
    if not top_scores_today or score.score > top_scores_today[-1].score or score.key() == top_scores_today[-1].key():
      return cls.NEW_TOP_SCORE_TODAY

    return cls.NEW_HIGH_SCORE

  @classmethod
  def check_for_top_time(cls, time, prev_best_time):
    # Grabbing top times (from other players)
    top_times = []
    for top_time in cls.get_best_times(time.game, 6)[0]:
      if ((not prev_best_time or prev_best_time.time > top_time.time) and
           time.player_id != top_time.player_id):
        top_times.append(top_time)
    top_times = top_times[:5]

    if not top_times:
      return cls.NEW_NUM_ONE_TIME

    # checking for #1 time
    if time.time < top_times[0].time:
      cls.notify_number_one_time(time, top_times)
      return cls.NEW_NUM_ONE_TIME

    # checking for top time
    if time.time < top_times[-1].time:
      cls.notify_top_time(time, top_times)
      return cls.NEW_TOP_TIME

    # checking for top time today
    top_times_today = cls.get_best_times(time.game, 5, True)[0]
    if not top_times_today or time.time > top_times_today[-1].time or time.key() == top_times_today[-1].key():
      return cls.NEW_TOP_TIME_TODAY

    return cls.NEW_BEST_TIME

  @classmethod
  def notify_number_one_score(cls, score, top_scores):
    event = PlayerEvent(parent=Player.get_key(top_scores[0].player_id))
    event.game = score.game
    event.reference=score.key()
    event.event = cls.NUM_ONE_SCORE_BEATEN
    event.detail = cls.MESSAGES[event.event] % (Player.get(player_id=score.player_id).alias, top_scores[0].score, score.score, score.game)
    event_hub.send(event)
    cls.notify_top_score(score, top_scores)

  @classmethod
  def notify_top_score(cls, score, top_scores):
    for top_score in top_scores[1:]:
      if top_score.score < score.score:
        event = PlayerEvent(parent=Player.get_key(top_score.player_id))
        event.game = score.game
        event.reference=score.key()
        event.event = cls.TOP_SCORE_BEATEN
        event.detail = cls.MESSAGES[event.event] % (Player.get(player_id=score.player_id).alias, top_score.score, score.score, score.game)
        event_hub.send(event)

  @classmethod
  def notify_number_one_time(cls, time, top_times):
    event = PlayerEvent(parent=Player.get_key(top_times[0].player_id))
    event.game = time.game
    event.reference=time.key()
    event.event = cls.NUM_ONE_TIME_BEATEN
    event.detail = cls.MESSAGES[event.event] % (Player.get(player_id=time.player_id).alias, top_times[0].time, time.time, time.game) ;
    event_hub.send(event)
    cls.notify_top_time(time, top_times)

  @classmethod
  def notify_top_time(cls, time, top_times):
    for top_time in top_times[1:]:
      if top_time.time < time.time:
        event = PlayerEvent(parent=Player.get_key(top_time.player_id))
        event.game = time.game
        event.reference=time.key()
        event.event = cls.TOP_TIME_BEATEN % (Player.get(player_id=time.player_id).alias, top_time.time, time.time, time.game)
        event.detail = cls.MESSAGES[event.event];
        event_hub.send(event)

  @classmethod
  def build_change_query(cls, game, scores, events=(), friend_ids=None):
    query = []
    for score in scores:
      query.append('(event:"%s" AND player_id:"%s")' % (Player.ALIAS_CHANGED, score.player_id))

    if friend_ids is not None:
      query.append('(event:"%s")' % Friendship.FRIEND_REMOVED)
      query.append('(event:"%s")' % Friendship.FRIEND_REQUEST_ACCEPTED)
      for friend_id in friend_ids:
        query.append('(game:"%s" AND player_id:%s AND (event:"%s")' % (game, friend_id, '" OR event:"'.join(events)))
    else:
      for event in events:
        query.append('(game:"%s" AND event:"%s")' % (game, event))

    return '(%s)' % ' OR '.join(query)

  @classmethod
  def get_best_times(cls, game, count, today=False):
    query = cls.all()
    query.filter('game', game)
    query.filter('is_best_time', True)

    if today:
      query.filter('date', datetime.datetime.now().date())
    query.order('time')
    times = query.fetch(count)
    if today:
      events = (cls.NEW_TOP_TIME_TODAY, cls.NEW_NUM_ONE_TIME, cls.NEW_TOP_TIME)
    else:
      events = (cls.NEW_NUM_ONE_TIME, cls.NEW_TOP_TIME)
    return times, cls.build_change_query(game, times, events)

  @classmethod
  def get_best_friend_times(cls, player_id, game, count):
    friend_ids = [friend.friend_id for friend in Friendship.get_friendships(player_id)]
    friend_ids.append(player_id)

    times = []
    for friend_id in friend_ids:
      time = cls.get_best_time(friend_id, game)
      if time:
        times.append(time)

    times.sort(key=lambda time: time.time)
    times = times[:count]
    return times, cls.build_change_query(game, times, ("new", "time"), friend_ids)

  @classmethod
  def get_high_scores(cls, game, count, today=False):
    query = cls.all()
    query.filter('game', game)
    query.filter('is_high_score', True)

    if today:
      query.filter('date', datetime.datetime.now().date())
    query.order('-score')
    scores = query.fetch(count)
    if today:
      events = (cls.NEW_NUM_ONE_SCORE, cls.NEW_TOP_SCORE, cls.NEW_TOP_SCORE_TODAY)
    else:
      events = (cls.NEW_NUM_ONE_SCORE, cls.NEW_TOP_SCORE)
    return scores, cls.build_change_query(game, scores, events)


  @classmethod
  def get_high_friend_scores(cls, player_id, game, count):
    friend_ids = [friend.friend_id for friend in Friendship.get_friendships(player_id)]
    friend_ids.append(player_id)

    scores = []
    for friend_id in friend_ids:
      score = cls.get_high_score(friend_id, game)
      if score:
        scores.append(score)

    scores.sort(key=lambda score: (-score.score, score.player_id))
    scores = scores[:count]
    return scores, cls.build_change_query(game, scores, ("new", "score"), friend_ids)

  @classmethod
  def get_best_time(cls, player_id, game):
    player = Player.get_key(player_id)
    query = cls.all().ancestor(player)
    query.filter('game', game)
    query.order('time')
    return query.get()

  @classmethod
  def get_high_score(cls, player_id, game):
    player = Player.get_key(player_id)
    query = cls.all().ancestor(player)
    query.filter('game', game)
    query.order('-score')
    return query.get()


def send_notify_event(player_id, event, event_type, message):
  new_event = PlayerEvent(parent=Player.get_key(player_id))
  new_event.game = event.game
  new_event.reference = event.reference
  new_event.event = event_type
  new_event.int_value = event.int_value
  new_event.str_value = event.str_value
  new_event.detail = message
  event_hub.send(new_event)


def notify_friends(event):
  player = Player.get(player_id=event.player_id)
  friend_ids = [friendship.friend_id for friendship in Friendship.get_friendships(event.player_id)]
  if event.event == Player.ALIAS_CHANGED:
    message = Player.MESSAGES[Player.FRIEND_ALIAS_CHANGED] % (event.prev_value, event.str_value);
    for friend_id in friend_ids:
      send_notify_event(friend_id, event, Player.FRIEND_ALIAS_CHANGED, message)
  if event.event in [PlayerScore.NEW_HIGH_SCORE, PlayerScore.NEW_TOP_SCORE_TODAY, PlayerScore.NEW_TOP_SCORE, PlayerScore.NEW_NUM_ONE_SCORE]:
    message = PlayerScore.MESSAGES[PlayerScore.SCORE_BEATEN];
    for friend_id in friend_ids:
      # see if you beat the friends score
      score = PlayerScore.get_high_score(friend_id, event.game)
      if (score and  # friend has played this game
          score.score < event.int_value and  # new score is better than friend's
          # friend was beating old score
          (not event.prev_value or event.prev_value.score < score.score)):
        # beat the friends score
        send_notify_event(friend_id, event, PlayerScore.SCORE_BEATEN,
                          message % (player.alias,
                                     score.score,
                                     event.int_value,
                                     event.game))
  elif event.event in [PlayerScore.NEW_BEST_TIME, PlayerScore.NEW_TOP_TIME_TODAY, PlayerScore.NEW_TOP_TIME, PlayerScore.NEW_NUM_ONE_TIME]:
    message = PlayerScore.MESSAGES[PlayerScore.TIME_BEATEN];
    for friend_id in friend_ids:
      # see if you beat the friends score
      score = PlayerScore.get_best_time(friend_id, event.game)
      if (score and  # friend has played this game
          score.time > event.int_value and  # new score is better than friend's
          # friend was beating old score
          (not event.prev_value or event.prev_value.time > score.time)):
        # beat the friends score
        send_notify_event(friend_id, event, PlayerScore.TIME_BEATEN,
                          message % (player.alias,
                                     score.time,
                                     event.int_value,
                                     event.game))


